using System;
using System.Collections.Generic;
using System.Text;
using Tomahawk.Runtime.Rendering.Interfaces;
using Tomahawk.Runtime.Rendering.Materials;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using Tomahawk.Runtime.Resources;
using Microsoft.Xna.Framework.Content;
using Tomahawk.Runtime.Utils;
using System.Collections;

namespace Tomahawk.Runtime.Rendering.Objects
{


    /// <summary>
    /// 3D Model that consists of a group of meshes, each one using a customizable material
    /// </summary>
    public class MaterialMesh : MaterialEntity, IRenderable
    {
        #region NodeCollection

        public class NodeCollection : IEnumerable
        {
            protected MaterialMesh mesh;

            public NodeCollection(MaterialMesh mesh)
            {
                this.mesh = mesh;
            }
            public Matrix this[string index]
            {
                get
                {
                    ModelMesh model = null;
                    if (mesh.meshModel.Meshes.TryGetValue(index, out model))
                    {
                        int boneIndex = model.ParentBone.Index;
                        return mesh.meshModel.Bones[boneIndex].Transform;
                    }

                    return Matrix.Identity;
                }
                set
                {
                    ModelMesh model = null;
                    if (mesh.meshModel.Meshes.TryGetValue(index, out model))
                    {
                        int boneIndex = model.ParentBone.Index;
                        mesh.meshModel.Bones[boneIndex].Transform = value;
                        mesh.dirtyTransformations = true;
                    }
                }
            }

            #region IEnumerable Members

            public IEnumerator GetEnumerator()
            {
                return mesh.meshModel.Bones.GetEnumerator();
            }

            #endregion

        }
        #endregion

        #region MaterialCollection
        public class MaterialCollection
        {
            protected MaterialMesh mesh;

            public MaterialCollection(MaterialMesh mesh)
            {
                this.mesh = mesh;
            }
            public SASMaterial this[string index]
            {
                get
                {
                    ModelMesh model = null;
                    SASMaterial material = null;
                    if (mesh.meshModel.Meshes.TryGetValue(index, out model))
                    {
                        if (model.MeshParts.Count > 0)
                        {
                            ModelMeshPart part = model.MeshParts[0];
                            if (mesh.materialTable.TryGetValue(part, out material))
                            {
                                return material;
                            }
                        }
                    }

                    return null;
                }
            }
        }
        #endregion

        private enum VertexStreamType
        {
            OnePassPerMesh,
            OnePassPerPart,
        }

        public class MaterialMeshDefinition
        {
            public string meshPartName = null;
            public string materialName = null;
            public string diffuseMapName = null;
            public string normalMapName = null;
            public string specularMapName = null;
            public string environmentMapName = null;
            public string lightMapName = null;
        }

        private string modelAsset;
        private List<MaterialMeshDefinition> materialDefinitions = new List<MaterialMeshDefinition>();
        private Dictionary<ModelMeshPart, SASMaterial> materialTable = new Dictionary<ModelMeshPart, SASMaterial>();
        private List<SASMaterial> materialRenderCache = new List<SASMaterial>();

        private bool dirtyTransformations = true;
        private Matrix[] absoluteTransformations = null;

        private NodeCollection nodes = null;
        public NodeCollection Nodes { get { return nodes; } }

        private MaterialCollection materials = null;
        public MaterialCollection Materials { get { return materials; } }

        protected RenderManager.Layers layer = RenderManager.Layers.Standard;
        public RenderManager.Layers Layer { get { return layer; } set { layer = value; } }

        RenderCommand.ExecuteCommandDelegate ExecuteRenderCommandDelegate = null;

        /// <summary>
        /// Constructor
        /// </summary>
        public MaterialMesh(string modelAsset, RenderManager.Layers layer, List<MaterialMeshDefinition> materialDefinitions, ResourceManager.ResourcePool pool)
        {
            this.resourcePool = pool;

            this.modelAsset = modelAsset;
            this.layer = layer;
            this.nodes = new NodeCollection(this);
            this.materials = new MaterialCollection(this);
            this.ExecuteRenderCommandDelegate = new RenderCommand.ExecuteCommandDelegate(OnExecuteRenderCommand);

            foreach (MaterialMeshDefinition matDef in materialDefinitions)
            {
                MaterialMeshDefinition def = new MaterialMeshDefinition()
                {
                    meshPartName = matDef.meshPartName,
                    materialName = matDef.materialName,
                    diffuseMapName = matDef.diffuseMapName,
                    normalMapName = matDef.normalMapName,
                    specularMapName = matDef.specularMapName
                };
                this.materialDefinitions.Add(def);
            }
        }

        /// <summary>
        /// Model asset name
        /// </summary>
        public string ModelAsset
        {
            get { return modelAsset; }
        }

        public void SetNodeTransform(string nodeName, ref Matrix mat)
        {
            this.nodes[nodeName] = mat;
        }

        #region "Loading"

        /// <summary>
        /// Content Loading
        /// </summary>
        protected override bool OnLoad(GraphicsDevice graphics, ContentManager contentManager)
        {
            //load the model
            this.meshModel = contentManager.Load<Model>(this.modelAsset);

            //allocate the matrices for bone transformation
            absoluteTransformations = new Matrix[this.meshModel.Bones.Count];

            //for each mesh in the model
            foreach (ModelMesh mesh in this.meshModel.Meshes)
            {
                //cannot process model with nonamed meshed
                if (mesh.Name == "") throw new Exception("Cannot use MaterialMesh with models with unnamed meshes or parts.");

                //if there is an override defined for it
                SASMaterial mat = null;
                int idx = findMeshIndex(mesh.Name);
                if (idx != -1)
                {
                    //creates a material instance and initilizes it
                    mat = createMaterial(this.materialDefinitions[idx], contentManager);

                    if (mesh.MeshParts.Count == 0)
                    {
                        throw new Exception("The mesh has no parts");
                    }
                }
                else
                {
                    string fallbackName = Engine.Instance.EngineConfiguration._Fallback.MaterialName;
                    mat = Engine.Instance.MaterialManager.Load<SASEffect>(fallbackName, contentManager);

                    if (mat == null)
                    {
                        throw new Exception("You must specify a material override for mesh: " + mesh.Name);
                    }                    
                }

                if (mat != null)
                {
                    for (int partIdx = 0; partIdx < mesh.MeshParts.Count; ++partIdx)
                    {
                        int subIdx = findMeshIndex(mesh.Name + "_" + partIdx.ToString());
                        if (subIdx != -1)
                        {
                            SASMaterial subMat = createMaterial(this.materialDefinitions[subIdx], contentManager);
                            this.materialTable.Add(mesh.MeshParts[partIdx], subMat);
                            this.materialRenderCache.Add(subMat);
                        }
                        else
                        {
                            this.materialTable.Add(mesh.MeshParts[partIdx], mat);
                            this.materialRenderCache.Add(mat);
                        }
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Find the index for the given mesh part 
        /// </summary>
        private int findMeshIndex(string name)
        {
            int retVal = -1;
            for (int i = 0; i < this.materialDefinitions.Count; i++)
                if (this.materialDefinitions[i].meshPartName == name) retVal = i;
            return retVal;
        }

        private SASMaterial createMaterial(MaterialMeshDefinition def, ContentManager contentManager)
        {
            SASMaterial mat = Engine.Instance.MaterialManager.Load<SASEffect>(def.materialName, contentManager);

            if (mat == null)
            {
                throw new Exception("The mesh references an invalid material");
            }

            if ((def.diffuseMapName != null) && (def.diffuseMapName != ""))
            {
                mat.DiffuseMap = contentManager.Load<Texture>(def.diffuseMapName);
            }

            if ((def.normalMapName != null) && (def.normalMapName != ""))
            {
                mat.NormalMap = contentManager.Load<Texture>(def.normalMapName);
            }

            if ((def.specularMapName != null) && (def.specularMapName != ""))
            {
                mat.SpecularMap = contentManager.Load<Texture>(def.specularMapName);
            }

            if ((def.environmentMapName != null) && (def.environmentMapName != ""))
            {
                mat.EnvironmentMap = contentManager.Load<Texture>(def.environmentMapName);
            }

            if ((def.lightMapName != null) && (def.lightMapName != ""))
            {
                mat.LightMap = contentManager.Load<Texture>(def.lightMapName);
            }

            return mat;
        }

        #endregion

        #region "Rendering"

        /// <summary>
        /// Prepares the material for rendering 
        /// </summary>
        public void OnExecuteRenderCommand(SASMaterial material)
        {
            RaiseOnPrepareMaterial(this, material);
        }

        /// <summary>
        /// Render the model 
        /// </summary>
        public void Render(ref Matrix worldMatrix, GraphicsDevice myDevice)
        {
            if (this.dirtyTransformations)
            {
                this.meshModel.CopyAbsoluteBoneTransformsTo(absoluteTransformations);
                this.dirtyTransformations = false;
            }

            int materialId = 0;
            RenderManager manager = Engine.Instance.RenderManager;
            foreach (ModelMesh mesh in this.meshModel.Meshes)
            {
                int boneIndex = mesh.ParentBone.Index;

                foreach (ModelMeshPart part in mesh.MeshParts)
                {
                    SASMaterial material = this.materialRenderCache[materialId];
                    
                    if (material.Effect.IsTechniqueAvailable(SASEffect.currentRenderGroup))
                    {
                        Matrix world;
                        Matrix.Multiply(ref absoluteTransformations[boneIndex], ref worldMatrix, out world);

                        RenderCommand command = Engine.Instance.RenderManager.BuildRenderCommand(mesh, part, ref world, material);
                        command.OnExecuteCommand += ExecuteRenderCommandDelegate;
                        manager.AddCommand(command, layer);
                    }

                    ++materialId;
                }
            }
        }
        #endregion

    }

}
